package net.ikenway.springboot.demo.oauth;

import com.google.gson.Gson;
import net.ikenway.springboot.demo.oauth.Entity.AccessTokenResponse;
import net.ikenway.springboot.demo.oauth.Entity.User;
import org.springframework.http.*;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;

/**
 * @author MasterKenway <zk@ikenway.net>
 * @Description
 * @date Created Date 12/3/2020
 */
public class GithubOAuth2LoginConfigurer<H extends HttpSecurityBuilder<H>> extends
        AbstractHttpConfigurer<GithubOAuth2LoginConfigurer<H>, H> {
    static final String CLIENT_ID = "b4345fecafebbf43d393";
    static final String CLIENT_SECRET = "c5f20680ae7339a3008e8b26e81be15e7f9b299f";

    @Override
    public void configure(H http) throws Exception {
        http
                .addFilterAfter(new GithubOAuthRedirectFilter(), SecurityContextPersistenceFilter.class)
                .addFilterAfter(new GithubOAuth2LoginAuthenticationFilter(), SecurityContextPersistenceFilter.class);
    }

    static class GithubOAuthRedirectFilter extends OncePerRequestFilter {
        private static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth/github";
        private static final String REDIRECT_URI = "http://127.0.0.1:8080/login/oauth2/github/";

        @Override
        protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
            if (!httpServletRequest.getRequestURI().endsWith(DEFAULT_AUTHORIZATION_REQUEST_BASE_URI)) {
                filterChain.doFilter(httpServletRequest, httpServletResponse);
                return;// 执行完安全过滤器链后不执行后面的代码。
            }

            httpServletResponse.sendRedirect("https://github.com/login/oauth/authorize"
                    + "?client_id=" + CLIENT_ID
                    + "&&redirect_uri=" + REDIRECT_URI);
        }
    }

    static class GithubOAuth2LoginAuthenticationFilter extends OncePerRequestFilter {
        private static final String DEFAULT_CALLBACK_BASE_URI = "/login/oauth2/github/";
        private static final String DEFAULT_LOGIN_SUCCESS_REDIRECT_URL = "/user";
        private static final ProviderManager providerManager = new ProviderManager(Collections.singletonList(new GithubOAuth2AuthenticationProvider()));

        @Override
        protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
            String code = httpServletRequest.getParameter("code");
            if (!httpServletRequest.getRequestURI().endsWith(DEFAULT_CALLBACK_BASE_URI) || !StringUtils.hasText(code)) {
                filterChain.doFilter(httpServletRequest, httpServletResponse);
                return;
            }

            // Spring Security基本校验流程
            GithubOAuth2LoginAuthenticationToken githubOAuth2LoginAuthenticationToken = new GithubOAuth2LoginAuthenticationToken(code, httpServletRequest);
            Authentication successAuthentication;
            try {
                successAuthentication = providerManager.authenticate(githubOAuth2LoginAuthenticationToken);
                SecurityContextHolder.getContext().setAuthentication(successAuthentication);
                // 改变 session id (只是改id,没有删除session)
                httpServletRequest.changeSessionId();
                // 移除之前认证时的错误信息
                httpServletRequest.getSession().removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
                // 成功登录后，设置重定向到页面。
//                redirectStrategy(httpServletRequest, httpServletResponse);
                httpServletResponse.sendRedirect("/user");
            } catch (AuthenticationException authenticationException) {
                authenticationException.printStackTrace();
            }


        }

        private void redirectStrategy(HttpServletRequest request, HttpServletResponse response) throws IOException {
            String redirectUrl = DEFAULT_LOGIN_SUCCESS_REDIRECT_URL;
            SavedRequest savedRequest = (SavedRequest) request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST");
            if (Objects.nonNull(savedRequest))
                redirectUrl = savedRequest.getRedirectUrl();
            new DefaultRedirectStrategy().sendRedirect(request, response, redirectUrl);
        }

    }

    static class GithubOAuth2AuthenticationProvider implements AuthenticationProvider {

        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            GithubOAuth2LoginAuthenticationToken githubOAuth2LoginAuthenticationToken = (GithubOAuth2LoginAuthenticationToken) authentication;

            String accessToken = getAccessToken(githubOAuth2LoginAuthenticationToken.getCode());

            User user = getUserInfo(accessToken);

            return createSuccessAuthentication(Objects.requireNonNull(user), githubOAuth2LoginAuthenticationToken.getHttpServletRequest());
        }

        private String getAccessToken(String code) {
            RestTemplate restTemplate = new RestTemplate();
            Gson gson = new Gson();

            Map<String, String> map = new HashMap<>();
            map.put("client_id", CLIENT_ID);
            map.put("client_secret", CLIENT_SECRET);
            map.put("code", code);

            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.set("Accept", "application/json");

            RequestEntity<Map<String, String>> requestEntity = RequestEntity.post("https://github.com/login/oauth/access_token").headers(httpHeaders).body(map);
            ResponseEntity<String> response = restTemplate.exchange(requestEntity, String.class);
            AccessTokenResponse accessTokenResponse = gson.fromJson(response.getBody(), AccessTokenResponse.class);
            return accessTokenResponse.getAccessToken();
        }

        private User getUserInfo(String accessToken) {
            RestTemplate restTemplate = new RestTemplate();
            Gson gson = new Gson();
            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.setContentType(MediaType.APPLICATION_JSON);
            httpHeaders.set("Authorization", "Bearer " + accessToken);

            RequestEntity<Void> requestEntity = RequestEntity.get("https://api.github.com/user").headers(httpHeaders).build();
            ResponseEntity<String> userinfoResponse = restTemplate.exchange(requestEntity, String.class);
            return gson.fromJson(userinfoResponse.getBody(), User.class);
        }

        private Authentication createSuccessAuthentication(User userInfo, HttpServletRequest request) {
            User user = new User(userInfo.getLogin(), "", AuthorityUtils.createAuthorityList("USER"));
            GithubOAuth2LoginAuthenticationToken authenticationToken = new GithubOAuth2LoginAuthenticationToken(user, AuthorityUtils.createAuthorityList("USER"));
            // 设置认证用户的额外信息，比如 IP 地址、经纬度等。下面代码将赋值一个WebAuthenticationDetails对象，它的构造函数是request,会封装HttpServletRequest的信息。
            AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
            authenticationToken.setDetails(authenticationDetailsSource.buildDetails(request));
            return authenticationToken;
        }

        @Override
        public boolean supports(Class<?> aClass) {
            return GithubOAuth2LoginAuthenticationToken.class.isAssignableFrom(aClass);
        }
    }

    static class GithubOAuth2LoginAuthenticationToken extends AbstractAuthenticationToken {
        private String code;
        private Object principal;
        private HttpServletRequest httpServletRequest;


        public GithubOAuth2LoginAuthenticationToken(String code, HttpServletRequest httpServletRequest) {
            super(Collections.emptyList());
            this.code = code;
            this.httpServletRequest = httpServletRequest;
        }

        public GithubOAuth2LoginAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
            super(authorities);
            this.principal = principal;
            setAuthenticated(true);
        }

        public String getCode() {
            return code;
        }

        public HttpServletRequest getHttpServletRequest() {
            return httpServletRequest;
        }

        @Override
        public Object getCredentials() {
            return "";
        }

        @Override
        public Object getPrincipal() {
            return principal;
        }
    }
}
